package org.peterf.foxish.program;

import java.nio.Buffer;

import org.peterf.foxish.R;
import org.peterf.foxish.buffer.IndexBuffer;
import org.peterf.foxish.buffer.VertexBuffer;
import org.peterf.foxish.camera.Camera;
import org.peterf.foxish.opengl20.OpenGLUtil;
import org.peterf.foxish.texture.Texture;
import org.peterf.foxish.util.EngineException;
import org.peterf.foxish.util.ResourceUtil;

import android.opengl.GLES20;

/**
 * Class representing a shader program. In order to render primitives with this
 * program, there must exist exactly a {@link VertexBuffer} and an
 * {@link IndexBuffer}.
 * <p>
 * A typical use of program includes the following:
 * <li>Call {@link #setBuffer(VertexBuffer, IndexBuffer)} during creation time
 * to assign the buffers source.</li>
 * <li>Call {@link #use(Camera)} during rendering time to make this used.</li>
 * <li>Call {@link #assignAttrib(int, int, int)} to specify attribute fetching.</li>
 * <li>Call {@link #drawTriangles(int, int)} etc. to draw.</li>
 * <p>
 * Remember to call {@link Program#delete(Program)} to release unused program.
 * <p>
 * If the OpenGL context is lost, simply create another instance. You do not
 * need to call the previous one's {@link #delete(Program)} in this case.
 * 
 * @author peter
 * 
 */
public class Program {

    public static Program build(int vertexRes, int fragmentRes) {
        VertexShader v = VertexShader.compile(vertexRes);
        if (v == null) {
            throw new EngineException(R.string.foxish_err_vertex_fail);
        }
        FragmentShader f = FragmentShader.compile(fragmentRes);
        if (f == null) {
            throw new EngineException(R.string.foxish_err_fragment_fail);
        }
        Program p = link(v, f);
        if (p == null) {
            throw new EngineException(R.string.foxish_err_link_fail);
        }
        return p;
    }

    public static Program build(String vertex, String fragment) {
        VertexShader v = VertexShader.compile(vertex);
        if (v == null) {
            throw new EngineException(R.string.foxish_err_vertex_fail);
        }
        FragmentShader f = FragmentShader.compile(fragment);
        if (f == null) {
            throw new EngineException(R.string.foxish_err_fragment_fail);
        }
        Program p = link(v, f);
        if (p == null) {
            throw new EngineException(R.string.foxish_err_link_fail);
        }
        return p;
    }

    /**
     * Link the vertex shader and the fragment shader to build a program. The
     * vertex shader and the fragment shader is not deleted no matter whether
     * the link succeeds. If the build is successful and the vertex shader and
     * the fragment shader are no longer useful, use
     * {@link VertexShader#delete(VertexShader)} and
     * {@link FragmentShader#delete(FragmentShader)} to delete them.
     * 
     * @param v
     *            The vertex shader.
     * @param f
     *            The fragment shader.
     * @return The linked program. null if the build fails.
     */
    public static Program link(VertexShader v, FragmentShader f) {
        int program = OpenGLUtil.linkProgram(v.name, f.name);
        if (program == 0) {
            return null;
        } else {
            return new Program(program);
        }
    }

    /* package */int name;

    private IndexBuffer ib = null;

    private boolean shared = false;

    private Texture texture = null;

    private boolean valid = true;

    private VertexBuffer vb = null;

    /**
     * Construct a program with its name.
     * 
     * @param name
     *            The program name.
     */
    /* package */Program(int name) {
        this.name = name;
    }

    /**
     * Assign an attribute pointer position. Whether to use VBO is dependent on
     * the {@link VertexBuffer} previously attached with
     * {@link #setBuffer(VertexBuffer, IndexBuffer)}.
     * 
     * @param attribLoc
     *            The attribute location.
     * @param offset
     *            The offset in bytes into the buffer.
     * @param attribSize
     *            The number of elements in a single attribute.
     */
    public void assignAttrib(int attribLoc, int offset, int attribSize) {
        if (vb.hasVBO()) {
            OpenGLUtil.assignAttrib(attribLoc, attribSize, vb.getType()
                    .getType(), false, 0, offset);
        } else {
            Buffer buffer = vb.getData().position(offset);
            OpenGLUtil.assignAttrib(attribLoc, attribSize, vb.getType()
                    .getType(), false, 0, buffer);
        }
    }

    /**
     * Assign an attribute pointer position. Whether to use VBO is dependent on
     * the {@link VertexBuffer} previously attached with
     * {@link #setBuffer(VertexBuffer, IndexBuffer)}.
     * 
     * @param attribLoc
     *            The attribute location.
     * @param stride
     *            The stride in bytes.
     * @param offset
     *            The offset in bytes into the buffer.
     * @param attribSize
     *            The number of elements in a single attribute.
     */
    public void assignAttrib(int attribLoc, int stride, int offset,
            int attribSize) {
        if (vb.hasVBO()) {
            OpenGLUtil.assignAttrib(attribLoc, attribSize, vb.getType()
                    .getType(), false, stride, offset);
        } else {
            Buffer buffer = vb.getData().position(offset);
            OpenGLUtil.assignAttrib(attribLoc, attribSize, vb.getType()
                    .getType(), false, stride, buffer);
        }
    }

    public void assignUniformFloat(int loc, float value) {
        // TODO Remove GLES20 related.
        GLES20.glUniform1f(loc, value);
    }

    public void assignUniformMat4(int loc, float[] value, int offset) {
        OpenGLUtil.assignUniformMat4(loc, value, offset);
    }

    public void assignUniformVec4(int loc, float[] value, int offset) {
        // TODO Remove GLES20 related.
        GLES20.glUniform4fv(loc, 1, value, offset);
    }

    public void delete() {
        if (!shared) {
            OpenGLUtil.deleteProgram(name);
        }
    }

    /**
     * Draw triangles.
     * 
     * @param offset
     *            Offset in bytes into the index buffer.
     * @param n
     *            Number of indices to be read.
     */
    public void drawTriangles(int offset, int n) {
        // TODO Remove GLES20 related.
        if (ib.hasVBO()) {
            GLES20.glDrawElements(GLES20.GL_TRIANGLES, n, ib.getType()
                    .getType(), 0);
        } else {
            Buffer buffer = ib.getData().position(offset);
            GLES20.glDrawElements(GLES20.GL_TRIANGLES, n, ib.getType()
                    .getType(), buffer);
        }
    }

    /**
     * Draw triangle strips.
     * 
     * @param offset
     *            Offset in bytes into the index buffer.
     * @param n
     *            Number of indices to be read.
     */
    public void drawTriangleStrip(int offset, int n) {
        // TODO Remove GLES20 related.
        if (ib.hasVBO()) {
            GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, n, ib.getType()
                    .getType(), 0);
        } else {
            Buffer buffer = ib.getData().position(offset);
            GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, n, ib.getType()
                    .getType(), buffer);
        }
    }

    /**
     * Get the attribute location with its name resource id.
     * 
     * @param attribRes
     *            The resource id of the attribute name.
     * @return The location of the attribute.
     */
    public int getAttribLoc(int attribRes) {
        String attrib = ResourceUtil.getString(attribRes);
        return OpenGLUtil.getAttribLoc(name, attrib);
    }

    /**
     * Get the uniform location with its name resource id.
     * 
     * @param uniformRes
     *            The resource id of the uniform name.
     * @return The location of the uniform.
     */
    public int getUniformLoc(int uniformRes) {
        String uniform = ResourceUtil.getString(uniformRes);
        return OpenGLUtil.getUniformLoc(name, uniform);
    }

    public void invalidate() {
        valid = false;
    }

    public boolean isValid() {
        return valid;
    }

    /**
     * Set the {@link VertexBuffer} and the {@link IndexBuffer} this program
     * uses. This method is essential.
     * 
     * @param vb
     *            The vertex buffer.
     * @param ib
     *            The index buffer.
     */
    public void setBuffer(VertexBuffer vb, IndexBuffer ib) {
        this.vb = vb;
        this.ib = ib;
    }

    public void setTexture(Texture texture) {
        this.texture = texture;
    }

    /**
     * Make this program the current program in use.
     */
    public void use(Camera camera) {
        OpenGLUtil.useProgram(name);
        if (vb != null) {
            vb.use();
        }
        if (ib != null) {
            ib.use();
        }
        if (texture != null) {
            texture.use();
        }
        // TODO Camera will do it for better performance.
        String uniform = ResourceUtil.getString(R.string.uniform_camera);
        int loc = OpenGLUtil.getUniformLoc(name, uniform);
        OpenGLUtil.assignUniformMat4(loc, camera.getWorldToStdMat(), 0);
    }

    /* package */void relink(VertexShader v, FragmentShader f) {
        name = OpenGLUtil.linkProgram(v.name, f.name);
        valid = true;
    }

    /**
     * 
     * @return A shared program with the same program name.
     */
    /* package */Program shared() {
        Program p = new Program(name);
        p.shared = true;
        return p;
    }

}
